iT邦幫忙

2024 iThome 鐵人賽

DAY 16
2
JavaScript

可愛又迷人的 Web API系列 第 16

Day16. 自己架個 WebSocket Server 玩玩吧

  • 分享至 

  • xImage
  •  

我相信大家對 WebSocket 應該都不陌生,即使沒使用過,也能在網路上找到許多教學文章。因此今天想和大家分享的是,身為前端開發者,如果想要測試或深入了解 WebSocket 的運作方式,可以有哪些方法?其中一個方法就是:讓我們來親自架設 WebSocket Server 吧。

架設 WebSocket 伺服器

常用的是 Heroku,他可以方便部署 Node.js 應用程式,而且也是免費的,要拿來測試用也很方便。不過這次我想偷懶一下,改用 Glitch 這個有線上編輯器的部署平台,方便我快速建立 Node.js 應用程式,以下會用 Glitch 網站做範例介紹。

登入後選擇「New project」,他有幾個快速建立 app 的方案,我選的是 glitch-hello-node,基於 Node.js 和 Fastify 的應用程式,建立完後,就能開啟懶人模式,直接線上編輯啦 XXD。

https://mukiwu.github.io/web-api-demo/img/16-1.png

首先打開 Terminal,安裝 ws 套件

npm install ws

https://mukiwu.github.io/web-api-demo/img/16-2.png

server.js 設定 Fastify 伺服器,並整合 ws 庫,我還設了一個 Interval 讓他每一秒鐘發送訊息到前端頁面:

const fastify = require('fastify')({ logger: true });
const path = require('path');

// 配置 Fastify 靜態文件服務
fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'public'),
  prefix: '/',
});

// 設定 HTTP 路由(如果需要,可以顯示首頁)
fastify.get('/', async (request, reply) => {
  return { hello: 'world' };
});

// 啟動 HTTP 伺服器
const start = async () => {
  try {
    const server = await fastify.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
    console.log(`Server listening on ${server}`);
    
    // 啟動 WebSocket 伺服器
    const WebSocket = require('ws');
    const wss = new WebSocket.Server({ server: fastify.server });

    wss.on('connection', (ws) => {
      console.log('New client connected!');

      const interval = setInterval(() => {
       ws.send('Hello from the server!');
      }, 1000);

      ws.on('message', (message) => {
        console.log(`Received: ${message}`);
        ws.send(`You sent: ${message}`);
      });

      ws.on('close', () => {
        clearInterval(interval);
        console.log('Client has disconnected.');
      });
    });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

然後在前端建立 wss 連線,這邊要注意的是我前端的連線是 HTTPS,所以 WebSocket 的協議要改用 wss:// 而非 ws://,避免產生 Mix Content 的問題。

const socket = new WebSocket('wss://xxx.glitch.me/');

socket.onopen = () => {
  console.log('Connected to the WebSocket server');
};

socket.onmessage = (event) => {
  const messagesDiv = document.getElementById('messages');
  const newMessage = document.createElement('p');
  newMessage.textContent = `Received: ${event.data}`;
  messagesDiv.appendChild(newMessage);
};

socket.onclose = () => {
  console.log('WebSocket connection closed');
};

socket.onerror = (error) => {
  console.error('WebSocket error:', error);
};

關於 glitch 的 WebSocket url 可以從右上角的「Share」按鈕取得 https 的對外連結,再將 https 改為 wss 就是你的 WebSocket Server Url。

https://mukiwu.github.io/web-api-demo/img/16-3.png

打開網站後,可以從 console 面板看到 WebSocket 的連線狀況,建立連線後,WebSocket Server 就會使用 ws.send() 傳送訊息到前端,前端接受後會把訊息顯示在畫面上:

https://mukiwu.github.io/web-api-demo/img/16-4.gif

掌握 WebSocket 連線狀態的困難點

WebSocket 是一個單向通訊的服務,也就是當連線啟動時,我們前端只能被動的接收來自 WebSocket Server 的通知,但這也導致一個問題,就是我們可能不知道當沒收到通知時,究竟是網路問題?還是 WebSocket 本身的錯誤?這邊舉幾個 WebSocket 的特性來讓大家更了解發生的原因:

WebSocket 是非同步的

WebSocket 的連線狀態是隨時間動態變化的,我們只能檢查當下的連線狀態,例如我們在特定的時間點用 readyState 檢查連線狀態為 OPEN時,我們也沒辦法保證檢查的下一分鐘、下一秒的連線狀態還是 OPEN,這就會造成「無法即時掌握連線狀態」的問題。

此外,WebSocket 的連線是依賴於底層網路,而網路問題 (如封包遺失、延遲) 可能導致連線中斷或狀態異常。但瀏覽器或伺服器可能不會立即察覺到連線中斷,所以導致我們在檢查連線狀態時為 OPEN,但實際上資料傳輸已經中斷了。

缺乏雙向 Ping 機制

雖然前端只要不主動關閉連線,WebSocket 就會是持久連線,但 WebSocket 缺乏雙向的 ping 機制來主動檢查連線。伺服器雖然可以透過發送 ping 來檢查連線,但這依然是單向的,如果前端沒有實作類似的功能 (例如定期 ping ),那麼當連線中斷時,雙方可能都不會立即察覺,在這種情況下,就可能等到下一次嘗試發送訊息或進行操作時,才會發現連線已經中斷。

主動檢查 WebSocket 的連線狀態

透過前面的段落,可以發現幾個解決方案,例如設置定時器,定時檢查 readyState 的連線狀態;在 onerroronclose 事件中做一些處理邏輯,以便在連線異常時能立即應對;此外也能設定自動重連機制 ... 等等。這幾個解決方案,都是前端就能掌握的,相信大家有做過 WebSocket 都會做這些檢查,這邊就不贅述了。

而接下來要分享的 ping 機制,跟定時器很像,也是要定期去 ping 來檢查連線,ping 機制主要是寫在後端,用來確保前後端的 WebSocket 連線狀態。

ping 機制的實作原理為:Server 會定期發送 ping 給所有連線的 Client,如果 Client 有回應,就知道連線正常。如果在一定時間內沒有收到 ping 的回應,Server 就可以當作連線已經斷掉了,此時就能主動關閉連線。

我們來修改一下後端的程式碼,將這個 ws 套件的 ping() 方法加進去:

const fastify = require('fastify')({ logger: true });
const path = require('path');

fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'public'),
  prefix: '/',
});

fastify.get('/', async (request, reply) => {
  return { hello: 'world' };
});

const start = async () => {
  try {
    const server = await fastify.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
    console.log(`Server listening on ${server}`);
    
    const WebSocket = require('ws');
    const wss = new WebSocket.Server({ server: fastify.server });

    wss.on('connection', (ws) => {
      console.log('New client connected!');

      // 定期發送 ping
      const pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.ping();
          console.log('Ping sent');
        }
      }, 30000);

      const interval = setInterval(() => {
        ws.send('Hello from the server!');
      }, 1000);
      
	  // 當收到 Client 端的 pong 時
      ws.on('pong', () => {
        console.log('Pong received');
      });

      ws.on('message', (message) => {
        console.log(`Received: ${message}`);
        ws.send(`You sent: ${message}`);
      });

      ws.on('close', () => {
        clearInterval(interval);
        // 停止 ping
        clearInterval(pingInterval);
        console.log('Client has disconnected.');
      });
    });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();

ping()pong() 是用來檢查底層網路層的連線,不是用來傳遞消息的,所以不會在 Client 端的 message 事件中接收到,而會自動處理。也就是說,Client 端會自動回應 Server 端的 ping 消息,並發送一個 pong,但不會觸發 message 事件。

所以我們要看後端的 Log 來確認這個連線狀態,以 Glitch 為例,打開 Log 面板後,就能看到對應的訊息了

https://mukiwu.github.io/web-api-demo/img/16-5.png

範例程式碼

範例程式碼網址:https://mukiwu.github.io/web-api-demo/websocket.html

小結

以上,有任何問題歡迎留言討論唷。


上一篇
Day15. Web Workers API 的生命週期
下一篇
Day17. WebSocket 的處理訊息機制
系列文
可愛又迷人的 Web API31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Dylan
iT邦新手 1 級 ‧ 2024-09-24 09:12:45

pintpong 都是是自動處理的,那請問一段時間內沒收到 pong 後會斷開連線也是自動處理的嗎?🤔

MUKIwu iT邦新手 5 級 ‧ 2024-09-24 10:37:56 檢舉

是的,一段時間內沒收到 pong 之後自動斷開連線的處理,是由底層的 WebSocket 或其他網絡協議自動處理的。我們通常不需要手動去設定這些過程。

我要留言

立即登入留言